Skip to content

Merge builtins.wasm and builtins.wasi into one function#370

Merged
edolstra merged 4 commits intomainfrom
merge-wasm-wasi
Feb 27, 2026
Merged

Merge builtins.wasm and builtins.wasi into one function#370
edolstra merged 4 commits intomainfrom
merge-wasm-wasi

Conversation

@edolstra
Copy link
Collaborator

@edolstra edolstra commented Feb 26, 2026

Motivation

The new interface is

builtins.wasm {
  path = ./path/to/file.wasm;
  function = "fib";
} 33

This will allow other Wasm-related configuration to be added in the future (e.g. WASI features, memory limits, caching info, etc).

The argument is separate from the configuration because typically you want to write something like

let
  fib = builtins.wasm {
    path = ./path/to/file.wasm;
    function = "fib";
  };
in fib 33

It now also auto-detects whether a module uses WASI.

Context

Summary by CodeRabbit

  • Documentation

    • Reworked Wasm host docs into a single unified interface describing WASI vs non‑WASI modes, automatic WASI detection, updated examples, and clarified return/error semantics (WASI-only return path). Host imports now documented under the env module; removed prior dual‑interface narrative.
  • Refactor

    • Unified Wasm invocation into one configurable call accepting a config set (path + function) and an argument. Non‑WASI modules require an init export invoked on instantiation; WASI modules use standard WASI entry/argv semantics. Module caching simplified to per‑path; WASI IO routed through host logging.

The new interface is

  builtins.wasm {
    path = ./path/to/file.wasm;
    wasi = false;
    function = "fib";
  } 33
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Consolidates Wasm handling into a single primop (__wasm) with automatic WASI detection (by inspecting imports), splits execution into WASI vs non‑WASI modes, adds nix_wasm_init_v1() for non‑WASI modules, updates host imports to env, and simplifies instance caching to the wasm path.

Changes

Cohort / File(s) Summary
Documentation
doc/manual/source/protocols/wasm.md
Reworks docs to present a single builtins.wasm entry with WASI mode vs non‑WASI, removes builtins.wasi narrative, standardizes terminology, updates examples and documents nix_wasm_init_v1() and WASI return semantics.
Wasm primop implementation
src/libexpr/primops/wasm.cc
Replaces __wasi flow with unified __wasm primop taking a config attribute-set; auto-detects WASI by inspecting imports, stores per-module useWasi in NixWasmInstancePre, compiles during pre-init, changes caching key to wasmPath, routes WASI IO via WasiLogger, enforces nix_wasm_init_v1 for non‑WASI, and updates public registration/signature.

Sequence Diagram(s)

sequenceDiagram
    participant Evaluator as Nix Evaluator
    participant Parser as Config Parser
    participant Instantiator as Wasm Instantiator
    participant WasiRuntime as WASI Runtime
    participant NonWasiRuntime as Non‑WASI Runtime
    participant Module as Wasm Module

    Evaluator->>Parser: __wasm(config, arg)
    Parser->>Instantiator: validate config (path, function?)
    Instantiator->>Module: load & compile module
    Instantiator->>Instantiator: inspect imports -> detect `_start` => useWasi?

    alt useWasi == true
        Instantiator->>WasiRuntime: instantiate with WASI, wire return_to_nix
        WasiRuntime->>Module: call _start(argv[1])
        Module-->>WasiRuntime: invoke return_to_nix(result)
        WasiRuntime-->>Evaluator: deliver result
    else useWasi == false
        Instantiator->>NonWasiRuntime: instantiate without WASI
        NonWasiRuntime->>Module: call nix_wasm_init_v1()
        NonWasiRuntime->>Module: call configured function(valueId)
        Module-->>NonWasiRuntime: return i32 valueId
        NonWasiRuntime-->>Evaluator: deliver result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • cole-h

Poem

🐰 I hopped from two to one, a tidy little trick,
_start whispers WASI, else nix_wasm_init_v1 does the pick.
One primop calls the meadow, one path to fetch the prize,
I nibble bits and bounce — the result returns, surprise! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: consolidating two separate builtins (builtins.wasm and builtins.wasi) into a single function interface.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch merge-wasm-wasi

Comment @coderabbitai help to get the list of available commands and usage tips.

@grahamc
Copy link
Member

grahamc commented Feb 26, 2026

Maybe we should make the toggle not a boolean but an enum, like mode = "wasi" | "wasm" for a potential future where they rev the spec, or some secret third "was*" option come about? Also, maybe changing it from builtins.wasm to builtins.webassembly?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
doc/manual/source/protocols/wasm.md (1)

38-41: Clarify nix_wasm_init_v1() call frequency wording.

“called once when the module is instantiated” can read as global/per-module caching behavior. Consider clarifying it is once per instance (i.e., per builtins.wasm invocation).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/manual/source/protocols/wasm.md` around lines 38 - 41, Clarify the
wording for nix_wasm_init_v1() to state it is invoked once per module instance,
not globally; update the sentence mentioning `nix_wasm_init_v1()` to read that
the function is called once for each instantiation of the module (i.e., once per
`builtins.wasm` invocation / per instance), so readers understand it’s
per-instance initialization rather than a single global call across the process.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/libexpr/primops/wasm.cc`:
- Around line 614-635: The WASI configuration is being applied after
instantiateWasm(), but Wasmtime requires setting the WASI context on the store
before module instantiation; move the WasiConfig creation and calls to
wasi_config_set_stdout_custom/wasi_config_set_stderr_custom,
WasiConfig::argv(...) and the call to instance.wasmStore.context().set_wasi(...)
to occur before calling instantiateWasm(), then call instantiateWasm(state,
wasmPath, useWasi) (and only afterward use instance.addValue and
instance.getExport<Func>().call(...)); ensure the WasiLogger, its trampoline,
and argv are created and passed into the store via set_wasi prior to
instantiation so the module start can safely use WASI.

---

Nitpick comments:
In `@doc/manual/source/protocols/wasm.md`:
- Around line 38-41: Clarify the wording for nix_wasm_init_v1() to state it is
invoked once per module instance, not globally; update the sentence mentioning
`nix_wasm_init_v1()` to read that the function is called once for each
instantiation of the module (i.e., once per `builtins.wasm` invocation / per
instance), so readers understand it’s per-instance initialization rather than a
single global call across the process.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c3725 and 8e0ba58.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

@github-actions github-actions bot temporarily deployed to pull request February 26, 2026 14:46 Inactive
@edolstra
Copy link
Collaborator Author

@grahamc It now auto-detects whether to use WASI.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/libexpr/primops/wasm.cc (1)

607-642: ⚠️ Potential issue | 🔴 Critical

Configure WASI before instance instantiation.

Line 607 instantiates the module before Line 641 calls set_wasi. If the module executes start-time code during instantiation, this ordering can fail before WASI is available. This appears to be the same unresolved issue raised earlier.

In Wasmtime C++/C APIs, should `Store::Context::set_wasi(...)` be called before `instantiate` when start-time code may run?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libexpr/primops/wasm.cc` around lines 607 - 642, The code currently calls
instantiateWasm before configuring WASI which can run module start-time code
without WASI available; move the WASI setup so that when instance.pre->useWasi
is true you create a WasiConfig, set the custom stdout/stderr trampolines and
argv, call instance.wasmStore.context().set_wasi(std::move(wasiConfig)) before
calling instantiateWasm, and then call instantiateWasm (and remove the later
set_wasi call); look for instantiateWasm, instance.pre->useWasi, WasiConfig,
wasi_config_set_stdout_custom/wasi_config_set_stderr_custom, and
instance.wasmStore.context().set_wasi to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@doc/manual/source/protocols/wasm.md`:
- Line 355: Update the stale wording that says "wasi = true" to instead
reference WASI mode detection: change the sentence "This function is only
available when `wasi = true`" to something like "This function is only available
in WASI mode (module exports `_start`)" so it matches the document's
auto-detection behavior and references the `_start` export.

In `@src/libexpr/primops/wasm.cc`:
- Around line 98-103: The current heuristic that marks useWasi = true by
scanning module.exports() for the "_start" export is incorrect; instead detect
WASI by checking module.imports() for a WASI import module (e.g.,
"wasi_snapshot_preview1", "wasi_unstable", or imports whose module name starts
with "wasi_") and set useWasi based on that. Replace the export-based loop that
inspects module.exports() and the "_start" name with a loop over
module.imports() (or equivalent API) that looks at import.module() (or
import.module_name()) for known WASI module names and sets useWasi = true if
found, leaving all other logic (including the code that validates the "function"
attribute) untouched. Ensure you reference the same variables (useWasi, module)
and remove reliance on "_start" so WASI reactors and command modules are both
detected correctly.

---

Duplicate comments:
In `@src/libexpr/primops/wasm.cc`:
- Around line 607-642: The code currently calls instantiateWasm before
configuring WASI which can run module start-time code without WASI available;
move the WASI setup so that when instance.pre->useWasi is true you create a
WasiConfig, set the custom stdout/stderr trampolines and argv, call
instance.wasmStore.context().set_wasi(std::move(wasiConfig)) before calling
instantiateWasm, and then call instantiateWasm (and remove the later set_wasi
call); look for instantiateWasm, instance.pre->useWasi, WasiConfig,
wasi_config_set_stdout_custom/wasi_config_set_stderr_custom, and
instance.wasmStore.context().set_wasi to make these changes.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0ba58 and c9559dc.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc

@github-actions github-actions bot temporarily deployed to pull request February 26, 2026 15:45 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/libexpr/primops/wasm.cc (1)

135-144: ⚠️ Potential issue | 🟠 Major

WASI context is still configured after module instantiation.

The NixWasmInstance constructor instantiates the module at line 140 via pre->instancePre.instantiate(wasmCtx), but set_wasi() is called much later at line 641 in prim_wasm. While using instantiate_pre pre-compiles the module, the actual instantiation still happens in the constructor before WASI is configured. If the Wasm module has a start section (distinct from the _start export), it will execute during instantiation and may call WASI functions before the context is set, causing an abort.

Consider restructuring so that set_wasi() is called on the store context before NixWasmInstance is constructed, or defer instantiation until after WASI configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libexpr/primops/wasm.cc` around lines 135 - 144, The constructor
NixWasmInstance currently calls pre->instancePre.instantiate(wasmCtx) (in
NixWasmInstance(...)) before WASI is configured via set_wasi (done later in
prim_wasm), which allows a module start function to run without a proper WASI
context; fix by moving instantiation to after WASI is set: either (A) call
set_wasi on the store/context before constructing NixWasmInstance so the wasmCtx
passed into NixWasmInstance already has WASI, or (B) change NixWasmInstance to
defer instantiate (remove/replace instance =
unwrap(pre->instancePre.instantiate(wasmCtx)) from the constructor and add an
explicit instantiate() method invoked from prim_wasm after set_wasi completes);
update all call sites in prim_wasm to construct NixWasmInstance first only when
using approach A or to call the new instantiate() method after set_wasi when
using approach B, ensuring wasmCtx.set_data(this) is retained on the correct
store/context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@doc/manual/source/protocols/wasm.md`:
- Around line 43-52: The "WASI Mode" section currently says WASI is detected by
the module exporting `_start`, which contradicts the overview and non-WASI
section; update the "WASI Mode" text to state that WASI is detected by the
presence of `wasi_snapshot_preview1` imports, and optionally note that an
exported `_start` is just the conventional entrypoint used by WASI runtimes (but
not the detection mechanism). Adjust any example or wording in the "WASI Mode"
heading to reference `wasi_snapshot_preview1` as the detection mechanism and
keep `_start` described only as the entry point.

In `@src/libexpr/primops/wasm.cc`:
- Around line 671-701: The docstring for the "__wasm" primop (registered via
primop_wasm with implementation prim_wasm) inaccurately says WASI mode is
detected by checking for an exported "_start"; update the documentation to match
the implementation: state that WASI mode is detected by the presence of wasi
imports (e.g. "wasi_snapshot_preview1") rather than an exported "_start", and
clarify the rule about when the `function` attribute is required/forbidden
accordingly so the text aligns with how prim_wasm actually detects WASI.
- Around line 612-622: The error text thrown when instance.pre->useWasi is true
is misleading because WASI is detected via imports (wasi_snapshot_preview1) not
the presence of a '_start' export; update the Exception message thrown in the
builtins.wasm handling (the throw inside the if (instance.pre->useWasi) path
where functionAttr is provided) to reference WASI imports (e.g., "WASI modules
(modules that import 'wasi_snapshot_preview1') do not accept a 'function'
attribute") or similar wording that mentions imports rather than a '_start'
export; keep the same throw location and type (throw Error(...)) and ensure any
other related messages around functionAttr handling in this block (and the
non-WASI branch) remain consistent.

---

Duplicate comments:
In `@src/libexpr/primops/wasm.cc`:
- Around line 135-144: The constructor NixWasmInstance currently calls
pre->instancePre.instantiate(wasmCtx) (in NixWasmInstance(...)) before WASI is
configured via set_wasi (done later in prim_wasm), which allows a module start
function to run without a proper WASI context; fix by moving instantiation to
after WASI is set: either (A) call set_wasi on the store/context before
constructing NixWasmInstance so the wasmCtx passed into NixWasmInstance already
has WASI, or (B) change NixWasmInstance to defer instantiate (remove/replace
instance = unwrap(pre->instancePre.instantiate(wasmCtx)) from the constructor
and add an explicit instantiate() method invoked from prim_wasm after set_wasi
completes); update all call sites in prim_wasm to construct NixWasmInstance
first only when using approach A or to call the new instantiate() method after
set_wasi when using approach B, ensuring wasmCtx.set_data(this) is retained on
the correct store/context.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9559dc and 9a660b9.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc

@github-actions github-actions bot temporarily deployed to pull request February 26, 2026 16:30 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
doc/manual/source/protocols/wasm.md (1)

45-45: ⚠️ Potential issue | 🟡 Minor

Inconsistent wording for WASI detection mechanism.

Line 45 uses "imports a wasi_snapshot_preview1 function" while lines 15 and 28 consistently use "imports from wasi_snapshot_preview1". The latter phrasing is more accurate since wasi_snapshot_preview1 is the module name from which WASI functions are imported.

📝 Suggested fix for consistency
-WASI mode is automatically used when the module imports a `wasi_snapshot_preview1` function.
+WASI mode is automatically used when the module imports from `wasi_snapshot_preview1`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/manual/source/protocols/wasm.md` at line 45, Replace the inconsistent
phrasing "imports a `wasi_snapshot_preview1` function" with the consistent and
accurate wording "imports from `wasi_snapshot_preview1`" so the sentence reads:
"WASI mode is automatically used when the module imports from
`wasi_snapshot_preview1`." Update the occurrence matching that exact phrase to
match lines that use "imports from `wasi_snapshot_preview1`" elsewhere in the
document.
🧹 Nitpick comments (1)
doc/manual/source/protocols/wasm.md (1)

38-41: Add function signature for nix_wasm_init_v1().

Line 40 introduces the nix_wasm_init_v1() requirement but doesn't specify its signature. For consistency with line 41 (which specifies the entry point signature as fn(arg: u32) -> u32), consider adding the signature for nix_wasm_init_v1().

📋 Suggested enhancement
-Every Wasm module used in non-WASI mode must export:
-- A `memory` object that the host can use to read/write data.
-- `nix_wasm_init_v1()`, a function that is called once when the module is instantiated.
-- The entry point function, whose name is specified by the `function` attribute. It takes a single `ValueId` and returns a single `ValueId` (i.e. it has type `fn(arg: u32) -> u32`).
+Every Wasm module used in non-WASI mode must export:
+- A `memory` object that the host can use to read/write data.
+- `nix_wasm_init_v1()`, a function with signature `fn()` (no parameters or return value) that is called once when the module is instantiated.
+- The entry point function, whose name is specified by the `function` attribute. It takes a single `ValueId` and returns a single `ValueId` (i.e. it has type `fn(arg: u32) -> u32`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/manual/source/protocols/wasm.md` around lines 38 - 41, Add a clear
signature for the required init function: state that nix_wasm_init_v1 must be
exported with the signature fn() -> u32 (or specify the exact parameter/return
types expected) so it matches the style used for the entry point; update the
sentence mentioning nix_wasm_init_v1 to explicitly list its function signature
(referencing the symbol nix_wasm_init_v1 and contrast with the entry point named
by the function attribute) and ensure the documentation still lists memory and
the entry point signature fn(arg: u32) -> u32.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@doc/manual/source/protocols/wasm.md`:
- Line 45: Replace the inconsistent phrasing "imports a `wasi_snapshot_preview1`
function" with the consistent and accurate wording "imports from
`wasi_snapshot_preview1`" so the sentence reads: "WASI mode is automatically
used when the module imports from `wasi_snapshot_preview1`." Update the
occurrence matching that exact phrase to match lines that use "imports from
`wasi_snapshot_preview1`" elsewhere in the document.

---

Nitpick comments:
In `@doc/manual/source/protocols/wasm.md`:
- Around line 38-41: Add a clear signature for the required init function: state
that nix_wasm_init_v1 must be exported with the signature fn() -> u32 (or
specify the exact parameter/return types expected) so it matches the style used
for the entry point; update the sentence mentioning nix_wasm_init_v1 to
explicitly list its function signature (referencing the symbol nix_wasm_init_v1
and contrast with the entry point named by the function attribute) and ensure
the documentation still lists memory and the entry point signature fn(arg: u32)
-> u32.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a660b9 and a6a622b.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/libexpr/primops/wasm.cc

@github-actions github-actions bot temporarily deployed to pull request February 27, 2026 16:36 Inactive
@edolstra edolstra added this pull request to the merge queue Feb 27, 2026
Merged via the queue into main with commit 3306013 Feb 27, 2026
48 of 51 checks passed
@edolstra edolstra deleted the merge-wasm-wasi branch February 27, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants